Skip to content

Open PGN files from file explorer, messages,... in the App on Android#2671

Merged
veloce merged 3 commits intolichess-org:mainfrom
HaonRekcef:open-pgn-in-app
Mar 3, 2026
Merged

Open PGN files from file explorer, messages,... in the App on Android#2671
veloce merged 3 commits intolichess-org:mainfrom
HaonRekcef:open-pgn-in-app

Conversation

@HaonRekcef
Copy link
Copy Markdown
Collaborator

@HaonRekcef HaonRekcef commented Feb 22, 2026

closes #2425
Allows to open .pgn files from outside the app on android. Preview:

open_pgn_files.mp4

Successfully tested on Android 8 and Android 15

I used the package receive_sharing_intent which seems to only be sporadically maintained, but I couldnt find a better option.
I needed to give a direct link to github for the package because of a fix that is not released yet: KasemJaffer/receive_sharing_intent#333

Needs to be implemented on the iOS side as well, but I don't have the devices to do it.

I changed the launchmode to single task to prevent multiple instances of the app getting created.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds the ability to open .pgn files from outside the Lichess mobile app on Android, allowing users to share or open PGN files from file explorers, messaging apps, and other sources directly into the app.

Changes:

  • Added the receive_sharing_intent package (via git dependency) to handle incoming file shares on Android
  • Refactored PGN handling logic in ImportPgnScreen to be reusable from the app entry point
  • Updated Android manifest to register intent filters for PGN files and changed launch mode to singleTask

Reviewed changes

Copilot reviewed 4 out of 5 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pubspec.yaml Added receive_sharing_intent dependency from a specific GitHub commit
pubspec.lock Lock file update for the new dependency
lib/src/view/more/import_pgn_screen.dart Refactored PGN handling logic into a public static method for reuse
lib/src/app.dart Added sharing intent initialization and file processing logic
android/app/src/main/AndroidManifest.xml Added intent filters for PGN files and changed launch mode to singleTask
Comments suppressed due to low confidence (10)

lib/src/view/more/import_pgn_screen.dart:30

  • The shorthand enum syntax .error is inconsistent with the rest of the codebase. The codebase consistently uses the fully-qualified SnackBarType.error format. Please change type: .error to type: SnackBarType.error.
        showSnackBar(context, context.l10n.invalidPgn, type: .error);

lib/src/view/more/import_pgn_screen.dart:43

  • The shorthand enum syntax .white is inconsistent with the rest of the codebase. The codebase consistently uses the fully-qualified Side.white format. Please change orientation: .white to orientation: Side.white.
              orientation: .white,

android/app/src/main/AndroidManifest.xml:54

  • The second intent-filter for PGN files (lines 46-54) lacks MIME type specification and only uses pathSuffix. This is problematic because:
  1. The pathSuffix filter alone may not work reliably across all Android versions and file providers
  2. It could potentially match unintended files with .pgn in their names
  3. It creates duplicate intent handling with the first filter

Consider either removing this filter (if the MIME type filter at lines 36-44 is sufficient) or adding a MIME type to this filter as well for better reliability.

            <intent-filter>
                <action android:name="android.intent.action.VIEW" />
                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="android.intent.category.BROWSABLE" />

                <data android:scheme="content" />

                <data android:pathSuffix=".pgn" />
            </intent-filter>

lib/src/app.dart:200

  • Error handling in _processSharedFiles silently fails with only a debug print. If file reading fails due to permissions, encoding issues, or the file being moved/deleted, the user receives no feedback. Consider showing a user-visible error message using showSnackBar similar to how errors are handled in ImportPgnScreen.handlePgnText. This would provide better user experience and consistency with the existing error handling patterns in the codebase.
    } catch (e) {
      debugPrint('Failed to process incoming file: $e');
    }

pubspec.yaml:74

  • The dependency uses a specific commit SHA (2cea396843cd3ab1b5ec4334be4233864637874e) rather than a branch or tag. While this ensures reproducibility, it's important to verify:
  1. That this commit exists and contains the necessary fix mentioned in the PR description
  2. That this is a reasonable long-term approach, as the package maintainer may force-push or delete commits
  3. Consider documenting in a comment why this specific commit is needed and what fix it includes

This helps future maintainers understand why a git dependency is used instead of a published version.

  receive_sharing_intent:
    git:
      url: https://github.com/KasemJaffer/receive_sharing_intent
      ref: 2cea396843cd3ab1b5ec4334be4233864637874e

android/app/src/main/AndroidManifest.xml:24

  • Changing launchMode from singleTop to singleTask is a significant behavioral change that affects how the app handles multiple instances and task management. With singleTask:
  • Only one instance of the activity exists in the system
  • New intents will be delivered to the existing instance via onNewIntent()
  • The activity becomes the root of its own task

While this is likely necessary for the sharing intent to work properly, be aware that:

  1. It changes how back navigation works - users may not return to the previous app after pressing back
  2. It affects how the app interacts with the recent apps list
  3. It may impact existing deep link handling behavior

Consider testing edge cases like: opening the app from a notification while it's already open, deep links when the app is in the background, and back navigation from various entry points.

            android:launchMode="singleTask"

lib/src/view/more/import_pgn_screen.dart:47

  • The shorthand enum syntax .standard is inconsistent with the rest of the codebase. The codebase consistently uses fully-qualified enum values. Please change .standard to Variant.standard.
              variant: rule != null ? Variant.fromRule(rule) : .standard,

lib/src/view/more/import_pgn_screen.dart:58

  • The shorthand enum syntax .error is inconsistent with the rest of the codebase. The codebase consistently uses the fully-qualified SnackBarType.error format. Please change type: .error to type: SnackBarType.error.
      showSnackBar(context, context.l10n.invalidPgn, type: .error);

lib/src/app.dart:187

  • The code only processes the first file from the shared files list (files.first.path). If a user shares multiple PGN files at once, only the first one will be processed and the rest will be silently ignored. Consider either:
  1. Processing all shared files sequentially
  2. Showing an error message if multiple files are shared
  3. Documenting this limitation

The current behavior could confuse users who attempt to share multiple PGN files at once.

  Future<void> _processSharedFiles(List<SharedMediaFile> files) async {
    if (files.isEmpty) return;
    final filePath = files.first.path;

lib/src/app.dart:183

  • The sharing intent functionality is initialized unconditionally but appears to be Android-only based on the PR description. While the receive_sharing_intent package may handle platform checks internally, it would be more explicit and maintainable to wrap the initialization in a platform check (e.g., if (Platform.isAndroid)) to prevent unnecessary initialization on iOS and other platforms. This also makes it clearer that this feature is currently Android-only until iOS implementation is added.
  void _initSharingIntent() {
    // Warm start
    _intentSub = ReceiveSharingIntent.instance.getMediaStream().listen((
      List<SharedMediaFile> value,
    ) {
      _processSharedFiles(value);
    });

    // Cold start
    ReceiveSharingIntent.instance.getInitialMedia().then((List<SharedMediaFile> value) {
      _processSharedFiles(value);
      ReceiveSharingIntent.instance.reset();
    });
  }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +185 to +191
Future<void> _processSharedFiles(List<SharedMediaFile> files) async {
if (files.isEmpty) return;
final filePath = files.first.path;
try {
final context = _navigatorKey.currentContext;
if (context == null || !context.mounted) return;

Copy link

Copilot AI Feb 22, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's a potential race condition in the cold start handling. The getInitialMedia() call is made in initState(), but at that point the navigator context might not be ready yet, causing the file processing to silently fail when _navigatorKey.currentContext is null. Consider adding a retry mechanism or deferring the cold start processing until after the first frame is built using WidgetsBinding.instance.addPostFrameCallback(). This would ensure the navigator is ready to handle the navigation.

Suggested change
Future<void> _processSharedFiles(List<SharedMediaFile> files) async {
if (files.isEmpty) return;
final filePath = files.first.path;
try {
final context = _navigatorKey.currentContext;
if (context == null || !context.mounted) return;
Future<void> _processSharedFiles(
List<SharedMediaFile> files, {
int attemptsLeft = 3,
}) async {
if (files.isEmpty) return;
final context = _navigatorKey.currentContext;
if (context == null || !context.mounted) {
if (attemptsLeft <= 0) {
return;
}
WidgetsBinding.instance.addPostFrameCallback((_) {
_processSharedFiles(
files,
attemptsLeft: attemptsLeft - 1,
);
});
return;
}
final filePath = files.first.path;
try {

Copilot uses AI. Check for mistakes.
@r3econ
Copy link
Copy Markdown
Contributor

r3econ commented Feb 22, 2026

Cool stuff! 😎 Once this gets merged I'll work on the iOS support for this 🍏

Copy link
Copy Markdown
Contributor

@veloce veloce left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Except for the launch mode change, nothing to comment.

android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:launchMode="singleTask"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure to understand all the implications of this change. Could you elaborate why you had to change this, and what was the "problem" with "singleTop".

According to the doc here: https://developer.android.com/guide/topics/manifest/activity-element#lmode, "singleTask" uses case are:

Specialized launches (not recommended for general use)

Then I'm not sure to understand all the differences between the launch modes in that doc. But when I see,

  • for "singleTop"

If an instance of the activity already exists at the top of the target task, the system routes the intent to that instance through a call to its onNewIntent() method, rather than creating a new instance of the activity.

  • for "singleTask"

The system creates the activity at the root of a new task or locates the activity on an existing task with the same affinity. If an instance of the activity already exists and is at the root of the task, the system routes the intent to existing instance through a call to its onNewIntent() method, rather than creating a new one.

I'm not sure why we need to switch to singleTask, looks like singleTop should be good, no?

@HaonRekcef
Copy link
Copy Markdown
Collaborator Author

HaonRekcef commented Mar 1, 2026

@veloce the behavior I do not like about singleTop is this:

single_top.mp4

It opens a second app in the file explorer itself, which I always find strange.

@veloce
Copy link
Copy Markdown
Contributor

veloce commented Mar 2, 2026

I see. We are kind of used to it with other applications like the browser, or a map app, though. But for lichess maybe it is strange.

@veloce
Copy link
Copy Markdown
Contributor

veloce commented Mar 2, 2026

Since it is the flutter default, I'm afraid changing it could break some other parts of the app.

I tend to think we should conservatively keep "singleTop" as this is not such a big deal that the app opens in the file explorer.

With the change to single task, I'm wondering for instance, what are the differences when: opening app from notifications (should be ok), split screens (not sure here), OAuth flow, etc?

@HaonRekcef
Copy link
Copy Markdown
Collaborator Author

Since it is the flutter default, I'm afraid changing it could break some other parts of the app.

I tend to think we should conservatively keep "singleTop" as this is not such a big deal that the app opens in the file explorer.

With the change to single task, I'm wondering for instance, what are the differences when: opening app from notifications (should be ok), split screens (not sure here), OAuth flow, etc?

I think all of those issues should actually be even less likely, because with singleTask there is only one instance of the app.

Also the package used, suggests this: <!--Set activity launchMode to singleTask, if you want to prevent creating new activity instance everytime there is a new intent.--> https://pub.dev/packages/receive_sharing_intent

@veloce
Copy link
Copy Markdown
Contributor

veloce commented Mar 3, 2026

We can test it for a while with "singleTask" and revert if there is an issue.

Another question: the screen recording you posted, was it in release or debug mode? The loading time for the cold start is quite slow I found and strangely we don't see the splash screen but a white screen.

@veloce veloce merged commit 02256de into lichess-org:main Mar 3, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature] Utilize android's intent/share functionality to import PGN

4 participants